 /*
  * Copyright (c) 2021, 2023, yishun.cn All rights reserved.
  *
  */
 package com.hmdp.utils;

 import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.hmdp.entity.Shop;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Component;

 import java.time.LocalDateTime;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;

 import static com.hmdp.utils.RedisConstants.*;

 /**
  * <p>Project: hm-dianping-com.hmdp.utils-CacheClient
  * <p>Powered by yishun On 2023-02-20 15:19:55
  *
  * @author yishun [2465734101@qq.com]
  * @version 1.0
  * @since 17
  */
 @Slf4j
 @Component
 public class CacheClient {
     private final StringRedisTemplate stringRedisTemplate;
     private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


     public CacheClient(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }

     /**
      * Description: 将任意的java对象序列化为json并存储在string类型的key中，并且可以设置过期时间
      *
      * @param key
      * @param value
      * @param time
      * @param unit
      */
     public void set(String key, Object value, Long time, TimeUnit unit) {
         stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);

     }

     /**
      * Description: 将任意的java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
      *
      * @param key
      * @param value
      * @param time
      * @param unit
      */
     public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
         //设置逻辑过期
         RedisData redisData = new RedisData();
         redisData.setData(value);
         redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
         //写入Redis
         stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
     }

     /**
      * Description: 根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
      *
      * @param keyPrefix
      * @param id
      * @param type
      * @param <R>
      * @param <ID>
      * @return
      */
     public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,
                                           Long time, TimeUnit unit) {
         String key = keyPrefix + id;
         //从Redis中查询
         String json = stringRedisTemplate.opsForValue().get(key);
         //判断是否存在
         if (StrUtil.isNotBlank(json)) {
             //存在，返回
             return JSONUtil.toBean(json, type);
         }
         //判断命中的是否为空值
         if (json != null) {
             return null;
         }
         //不存在，根据id数据库查询
         R r = dbFallback.apply(id);
         if (r == null) {
             //将空值写入Redis
             stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
             return null;
         }
         this.set(key, r, time, unit);
         return r;
     }


     /**
      * Description: 根据指定的key查询缓存，并反序列化为指定类型，利用逻辑过期来解决缓存击穿问题
      * @param keyPrefix
      * @param id
      * @param type
      * @param dbFallback
      * @param time
      * @param unit
      * @return
      * @param <R>
      * @param <ID>
      */
     public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,
                                             Long time, TimeUnit unit) {
         String key = CACHE_SHOP_KEY + id;
         //1.从redis查询缓存
         String shopJson = stringRedisTemplate.opsForValue().get(key);
         //2.判断是否存在
         if (StrUtil.isBlank(shopJson)) {
             return null;
         }
         //4.命中，需要判断过期时间，先把Json反序列化为对象
         RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
         R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
         LocalDateTime expireTime = redisData.getExpireTime();
         //5.判断是否过期
         if (expireTime.isAfter(LocalDateTime.now())) {
             //5.1未过期，直接返回
             return r;
         }
         //5.2已过期，需要缓存重建
         //6.缓存重建
         //6.1获取互斥锁
         String lockKey = LOCK_SHOP_KEY + id;
         //6.2判断是否获取锁成功
         boolean isLock = tryLock(lockKey);
         if (isLock) {
             //6.3成功，开启独立线程，实现缓存重建
             CACHE_REBUILD_EXECUTOR.submit(() -> {
                 try {
                     //查询数据库
                     R r1 = dbFallback.apply(id);
                     //写入Redis
                     this.setWithLogicalExpire(key, r1, time, unit);
                 } catch (Exception e) {
                     throw new RuntimeException(e);
                 } finally {
                     //释放锁
                     unlock(lockKey);
                 }
             });

         }
         //6.4返回过期商铺信息
         return r;
     }

     private boolean tryLock(String key) {
         Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
         return BooleanUtil.isTrue(flag);
     }

     private void unlock(String key) {
         stringRedisTemplate.delete(key);
     }
 }
